import * as THREE from "three";
import { Canvas, type ThreeElements } from "@react-three/fiber";
import React, {
  useEffect,
  useState,
  forwardRef,
  useMemo,
  type ReactNode,
  useCallback,
  Suspense,
} from "react";
import { useSpring, animated } from "@react-spring/three";
import { useJoystickControls } from "ecctrl";

const JoystickComponents = (props: EcctrlJoystickProps) => {
  /**
   * Preset values/components
   */
  let joystickCenterX: number = 0;
  let joystickCenterY: number = 0;
  let joystickHalfWidth: number = 0;
  let joystickHalfHeight: number = 0;
  let joystickMaxDis: number = 0;
  let joystickDis: number = 0;
  let joystickAng: number = 0;
  const touch1MovementVec2 = useMemo(() => new THREE.Vector2(), []);
  const joystickMovementVec2 = useMemo(() => new THREE.Vector2(), []);

  const [windowSize, setWindowSize] = useState({ innerHeight, innerWidth });
  const joystickDiv: HTMLDivElement =
    document.querySelector("#ecctrl-joystick")!;

  /**
   * Animation preset
   */
  const [springs, api] = useSpring(() => ({
    topRotationX: 0,
    topRotationY: 0,
    basePositionX: 0,
    basePositionY: 0,
    config: {
      tension: 600,
    },
  }));

  /**
   * Joystick component geometries
   */
  const joystickBaseGeo = useMemo(
    () => new THREE.CylinderGeometry(2.3, 2.1, 0.3, 32),
    []
  );
  const joystickStickGeo = useMemo(
    () => new THREE.CylinderGeometry(0.3, 0.3, 3, 6),
    []
  );
  const joystickHandleGeo = useMemo(
    () => new THREE.SphereGeometry(1.4, 8, 16),
    []
  );
  /**
   * Joystick component materials
   */
  const joystickBaseMaterial = useMemo(
    () => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.3 }),
    []
  );
  const joystickStickMaterial = useMemo(
    () => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.3 }),
    []
  );
  const joystickHandleMaterial = useMemo(
    () => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.7 }),
    []
  );

  /**
   * Joystick store setup
   */
  const setJoystick = useJoystickControls((state) => state.setJoystick);
  const resetJoystick = useJoystickControls((state) => state.resetJoystick);

  // Touch move function
  const onTouchMove = useCallback(
    (e: TouchEvent) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      const touch1 = e.targetTouches[0];

      const touch1MovementX = touch1.pageX - joystickCenterX;
      const touch1MovementY = -(touch1.pageY - joystickCenterY);
      touch1MovementVec2.set(touch1MovementX, touch1MovementY);

      joystickDis = Math.min(
        Math.sqrt(Math.pow(touch1MovementX, 2) + Math.pow(touch1MovementY, 2)),
        joystickMaxDis
      );
      joystickAng = touch1MovementVec2.angle();
      joystickMovementVec2.set(
        joystickDis * Math.cos(joystickAng),
        joystickDis * Math.sin(joystickAng)
      );
      const runState = joystickDis > joystickMaxDis * 0.7;

      // Apply animations
      api.start({
        topRotationX: -joystickMovementVec2.y / joystickHalfHeight,
        topRotationY: joystickMovementVec2.x / joystickHalfWidth,
        basePositionX: joystickMovementVec2.x * 0.002,
        basePositionY: joystickMovementVec2.y * 0.002,
      });

      // Pass valus to joystick store
      setJoystick(joystickDis, joystickAng, runState);
    },
    [api, windowSize]
  );

  // Touch end function
  const onTouchEnd = () => {
    // Reset animations
    api.start({
      topRotationX: 0,
      topRotationY: 0,
      basePositionX: 0,
      basePositionY: 0,
    });

    // Reset joystick store values
    resetJoystick();
  };

  // Reset window size function
  const onWindowResize = () => {
    setWindowSize({
      innerHeight: window.innerHeight,
      innerWidth: window.innerWidth,
    });
  };

  useEffect(() => {
    const joystickPositionX = joystickDiv.getBoundingClientRect().x;
    const joystickPositionY = joystickDiv.getBoundingClientRect().y;
    joystickHalfWidth = joystickDiv.getBoundingClientRect().width / 2;
    joystickHalfHeight = joystickDiv.getBoundingClientRect().height / 2;

    joystickMaxDis = joystickHalfWidth * 0.65;

    joystickCenterX = joystickPositionX + joystickHalfWidth;
    joystickCenterY = joystickPositionY + joystickHalfHeight;

    joystickDiv.addEventListener("touchmove", onTouchMove, { passive: false });
    joystickDiv.addEventListener("touchend", onTouchEnd);

    window.visualViewport!.addEventListener("resize", onWindowResize);

    return () => {
      joystickDiv.removeEventListener("touchmove", onTouchMove);
      joystickDiv.removeEventListener("touchend", onTouchEnd);
      window.visualViewport!.removeEventListener("resize", onWindowResize);
    };
  });

  return (
    <Suspense fallback="null">
      <animated.group
        position-x={springs.basePositionX}
        position-y={springs.basePositionY}
      >
        <mesh
          geometry={joystickBaseGeo}
          material={joystickBaseMaterial}
          rotation={[-Math.PI / 2, 0, 0]}
          {...props.joystickBaseProps}
        />
      </animated.group>
      <animated.group
        rotation-x={springs.topRotationX}
        rotation-y={springs.topRotationY}
      >
        <mesh
          geometry={joystickStickGeo}
          material={joystickStickMaterial}
          rotation={[-Math.PI / 2, 0, 0]}
          position={[0, 0, 1.5]}
          {...props.joystickStickProps}
        />
        <mesh
          geometry={joystickHandleGeo}
          material={joystickHandleMaterial}
          position={[0, 0, 4]}
          {...props.joystickHandleProps}
        />
      </animated.group>
    </Suspense>
  );
};

export const EcctrlJoystick = forwardRef<HTMLDivElement, EcctrlJoystickProps>(
  (props, ref) => {
    const joystickWrapperStyle: React.CSSProperties = {
      userSelect: "none",
      MozUserSelect: "none",
      WebkitUserSelect: "none",
      msUserSelect: "none",
      touchAction: "none",
      pointerEvents: "none",
      overscrollBehavior: "none",
      position: "fixed",
      zIndex: "9999",
      height: props.joystickHeightAndWidth || "200px",
      width: props.joystickHeightAndWidth || "200px",
      left: props.joystickPositionLeft || "0",
      bottom: props.joystickPositionBottom || "0",
    };

    return (
      <div ref={ref}>
        <div
          id="ecctrl-joystick"
          style={joystickWrapperStyle}
          onContextMenu={(e) => e.preventDefault()}
        >
          <Canvas
            shadows
            orthographic
            camera={{
              zoom: props.joystickCamZoom || 26,
              position: props.joystickCamPosition || [0, 0, 50],
            }}
          >
            <JoystickComponents {...props} />
            {props.children}
          </Canvas>
        </div>
      </div>
    );
  }
);

export type EcctrlJoystickProps = {
  // Joystick props
  children?: ReactNode;
  joystickPositionLeft?: number;
  joystickPositionBottom?: number;
  joystickHeightAndWidth?: number;
  joystickCamZoom?: number;
  joystickCamPosition?: [x: number, y: number, z: number];
  joystickBaseProps?: ThreeElements["mesh"];
  joystickStickProps?: ThreeElements["mesh"];
  joystickHandleProps?: ThreeElements["mesh"];

  // Touch buttons props
  buttonNumber?: number;
  buttonPositionRight?: number;
  buttonPositionBottom?: number;
  buttonHeightAndWidth?: number;
  buttonCamZoom?: number;
  buttonCamPosition?: [x: number, y: number, z: number];
  buttonGroup1Position?: [x: number, y: number, z: number];
  buttonGroup2Position?: [x: number, y: number, z: number];
  buttonGroup3Position?: [x: number, y: number, z: number];
  buttonGroup4Position?: [x: number, y: number, z: number];
  buttonGroup5Position?: [x: number, y: number, z: number];
  buttonLargeBaseProps?: ThreeElements["mesh"];
  buttonSmallBaseProps?: ThreeElements["mesh"];
  buttonTop1Props?: ThreeElements["mesh"];
  buttonTop2Props?: ThreeElements["mesh"];
  buttonTop3Props?: ThreeElements["mesh"];
  buttonTop4Props?: ThreeElements["mesh"];
  buttonTop5Props?: ThreeElements["mesh"];
};
